/**
 * Copyright (c) 2008, Julien PORTALIER
 * http://featherphp.googlecode.com/
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 */

var Autocompleter = {};

/**
 * Autocompletes a single token.
 * 
 * \code
 * # will request /tags/live/$token.json
 * new Autocompleter.Base($('tags'), '/tags/live');
 * \endcode
 * 
 * @param Input  input Field to autocomplete;
 * @param String url
 * @param Object options:
 *   minChars : minimum chars before autocompletion (defaults to 1);
 *   timeout  : time to wait before autocompletion (defaults to 500ms);
 */
Autocompleter.Base = new Class(
{
	Implements: Options,
	choices: [],
	index: 0, 
	
	initialize: function(input, url, options)
	{
		// params
		this.input = $(input);
		this.url   = (url.substr(-1, 1) == '/') ? url : url + '/';
		
		// options
		this.options = {
			minChars: 1,
			timeout:  500
		};
		this.setOptions(options);
		
		// events
		this.input.addEvents({
			'keypress': this.onKeypress.bindWithEvent(this),
			'blur':     this.cancel.bindWithEvent(this)
		});
	},
	
	createPicker: function()
	{
		this.picker = new Dialog.Picker(this.input, {
			'className':   'autocomplete',
			'autoDisplay': false
		});
	},
	
	
	// tokens
	
	getToken: function()
	{
		return this.input.value;
	},
	
	setToken: function(token)
	{
		this.input.value = token;
	},
	
	
	// events
	
	cancel: function()
	{
		if (this.picker) {
			this.picker.hide();
		}
	},
	
	onKeypress: function(evt)
	{
		switch(evt.key)
		{
			case 'down':  this.markChoice(Math.min(this.index + 1, this.choices.length - 1)); break;
			case 'up':    this.markChoice(Math.max(this.index - 1, 0)); break;
			case 'enter': this.onChoice(); break;
			case 'esc':   this.cancel();   break;
			case ',':     this.cancel();   return;
		}
		
		// stops event's propagation
		if (['down', 'up', 'enter', 'esc'].contains(evt.key))
		{
			evt.stop();
			return;
		}
		
		// search, except for certain cases
		if (!['down', 'up', 'left', 'right', 'enter', 'tab', 'delete', 'backspace'].contains(evt.key))
		{
			$clear(this.timer);
			this.timer = this.getChoices.delay(this.options.timeout, this);
		}
	},
	
	onMouseOver: function(evt)
	{
		var el = (evt.target.get('tag') == 'li') ?
			evt.target : evt.target.getParent('li');
		this.markChoice(el.get('index'));
	},
	
	
	// requests
	
	getChoices: function()
	{
		this.token = this.getToken();
		if (this.token.length >= this.options.minChars
			&& !(/^\s*$/.test(this.token))
			&& !this.ajax)
		{
			this.ajax = new Request.JSON(
			{
				'url':        this.url + encodeURIComponent(this.token) + '.json',
				'onComplete': this.updateChoices.bind(this)
			}).get();
		}
	},
	
	
	// choices
	
	updateChoices: function(choices)
	{
		this.choices = choices;
		this.showChoices();
		this.ajax = null;
	},
	
	showChoices: function()
	{
		// cleans up the list
		this.list = new Element('ul',
		{
			'events': {
				'mouseover': this.onMouseOver.bindWithEvent(this),
				'mousedown': this.onChoice.bindWithEvent(this)
			}
		});
		
		if (!this.picker) {
			this.createPicker();
		}
		
		// displays choices
		if (this.choices.length > 0)
		{
			for (var index = 0; index < this.choices.length; index++)
			{
				this.list.appendChild(new Element('li', {
					'index': index,
					'html':  this.createHTMLToken(this.choices[index])
				}));
			}
			this.markChoice();
			
			this.picker.setContent('element', this.list);
			this.picker.display();
		}
		else {
			this.picker.hide();
		}
	},
	
	createHTMLToken: function(choice)
	{
		return choice.replace(new RegExp('^(' + this.token + ')', 'i'), '<strong>$1</strong>');
	},
	
	markChoice: function(index)
	{
		this.index  = index || 0;
		var choices = this.list.getElements('li');
		var li      = choices[this.index];
		
		if (!li.hasClass('selected'))
		{
			choices.removeClass('selected');
			li.addClass('selected');
		}
	},
	
	onChoice: function()
	{
		this.setToken(this.getSelectedToken())
		this.cancel();
	},
	
	getSelectedToken: function()
	{
		return this.choices[this.index];
	}
});

/**
 * Autocompletes multiple tokens.
 * 
 * \code
 * # will request /tags/live/$token.json
 * new Autocompleter.Base($('tags'), '/tags/live');
 * \endcode
 * 
 * Option: tokenMark (defaults to ',')
 */
Autocompleter.Multi = new Class(
{
	Extends: Autocompleter.Base,
	tokens: [],
	tokenIndex: 0,
	
	initialize: function(input, url, options)
	{
		this.parent(input, url, options);
		this.options.tokenMark = this.options.tokenMark || ',';
	},
	
	getToken: function()
	{
		var token = this.input.value;
		this.tokenIndex = null;
		
		// multiple tokens?
		if (token.indexOf(this.options.tokenMark) > -1)
		{
			var pos = token.length;
			
			// gets caret position
			if (document.selection)
			{
				// IE
				this.input.focus();
				var sel = document.selection.createRange();
				sel.moveStart('character', -token.length);
				pos = sel.text.length;
			}
			else if (this.input.selectionStart || this.input.selectionStart == '0')
			{
				// firefox, opera (& safari?)
				pos = this.input.selectionStart;
			}
			
			// gets currently edited token
			this.tokenIndex = token.substr(0, pos).split(this.options.tokenMark).length - 1;
			token = token.split(this.options.tokenMark)[this.tokenIndex];
		}
		
		return token.trim();
	},
	
	setToken: function(token)
	{
		var value = this.input.value;
		var space = this.options.tokenMark + ' ';
		
		// multiple tokens?
		if (value.indexOf(this.options.tokenMark) > -1)
		{
			// gets tokens
			var tokens = value.split(this.options.tokenMark);
			
			// updates edited token
			tokens[this.tokenIndex] = token;
			
			value = '';
			for (var i = 0; i < tokens.length; i++)
			{
				tokens[i] = tokens[i].trim();
				if (tokens[i] != '') {
					value += tokens[i] + space;
				}
			}
			
			this.input.value = value;
		}
		else
		{
			// single token
			this.input.value = token + space;
		}
	}
});